#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""

Setup:

    >>> from tack.lib import response_replacement, default_env


The cookie_monster filter supplies a dictionary to its application.
If no cookies are sent in the request, this dictionary will be empty:

    >>> def verify_cookies_empty(environ, start_response):
    ...    assert environ['cookies'] == {}
    ...    start_response('200 OKAY', [])
    ...    return []    

    >>> state, start_response = response_replacement()
    >>> app = cookie_monster(verify_cookies_empty)
    >>> results = app(default_env(), start_response)
    >>> state[1]
    [('Cache-control', 'no-cache="set-cookie"'), ('Cache-control', 'private')]


If the filtered application sets values in the cookie dictionary,
those are sent as response headers:

    >>> def set_new_cookies(environ, start_response):
    ...    environ['cookies']['spam'] = 'no, thanks.'
    ...    environ['cookies']['ham'] = 'yes, please.'
    ...    start_response('200 OKAY', [])
    ...    return []

    >>> state, start_response = response_replacement()
    >>> app = cookie_monster(set_new_cookies)
    >>> results = app(default_env(), start_response)
    >>> state[1]
    [('Set-Cookie', 'ham=yes%2C%20please.; Path=/'), ('Set-Cookie', 'spam=no%2C%20thanks.; Path=/'), ('Cache-control', 'no-cache="set-cookie"'), ('Cache-control', 'private')]


If the filtered application deletes a cookie from the dictionary, the
cookie is invalidated and sent back:

    >>> def delete_some_cookies(environ, start_response):
    ...    del(environ['cookies']['ham'])
    ...    start_response('200 OKAY', [])
    ...    return []

    >>> state, start_response = response_replacement()
    >>> env = default_env(Cookie='ham="yummy"; spam="nasty"')
    >>> app = cookie_monster(delete_some_cookies)
    >>> results = app(env, start_response)
    >>> state[1]
    [('Set-Cookie', 'ham=; Max-Age=0; Path=/'), ('Cache-control', 'no-cache="set-cookie"'), ('Cache-control', 'private')]

When a filtered application modifies a cookie value, that value is
sent back to the client:

    >>> def modify_some_cookies(environ, start_response):
    ...    assert environ['cookies']['spam'] == '"no, thanks."'
    ...    environ['cookies']['spam'] = 'only if it comes with eggs.'
    ...    start_response('200 OKAY', [])
    ...    return []

    >>> env = default_env(Cookie='spam="no, thanks."; ham="yummy."')
    >>> state, start_response = response_replacement()
    >>> app = cookie_monster(modify_some_cookies)
    >>> results = app(env, start_response)
    >>> state[1]
    [('Set-Cookie', 'spam=only%20if%20it%20comes%20with%20eggs.; Path=/'), ('Cache-control', 'no-cache="set-cookie"'), ('Cache-control', 'private')]


Raw cookie values are available to the client application as well:

    >>> def raw_cookie_check(environ, start_response):
    ...    age = environ['wsgi.cookies']['eggs']['max-age']
    ...    assert strptime(age, expiry_format)[0:6] ==  (2007, 6, 13, 12, 34, 56)
    ...    start_response('200 OKAY', [])
    ...    return []
    
    >>> env = default_env(Cookie='eggs=some; Max-Age="Fri, 13-Jun-07 12:34:56 GMT"')
    >>> state, start_response = response_replacement()
    >>> app = cookie_monster(raw_cookie_check)
    >>> app(env, start_response)
    []


To support ciphering cookie values, we have to also include the auth module:

    >>> from tack.lib.cipher import make_ciphers, enc_default_key, base64zip_decode
    >>> enc, dec, ccp = make_ciphers(enc_default_key)


Use the cookie spec from http://www.ietf.org/rfc/rfc2965.  Here we
simulate a request:

    >>> env = default_env(Cookie='part_num=eJzriJcvkjwX+jFyjqPQZHnrlrZv29583+AZTS8aANosUh0=;part_desc=eJxjv/BhmvGd7W/UZh39d+M216K2b9vefN/gGU0vGgCE9lWw')
    >>> results = cookie_monster(string_app(''))(env, start_response)

    >>> sorted(env['cookies'].keys())
    ['part_desc', 'part_num']

    >>> dec(base64zip_decode( env['cookies']['part_num'] ))
    '15354865464'

    >> dec(base64zip_decode( env['cookies']['part_desc']  ))
    'Flamethrower'

    >>> env['cookies']['eggs']
    Traceback (most recent call last):
    ...
    KeyError: 'eggs'


"""
from time import strftime, strptime

from tack.lib import default_env, env_keys, response_replacement, string_app
from tack.lib.cookies import WsgiCookie


# expiry date format per rfc 2109:  Wdy, DD-Mon-YY HH:MM:SS GMT
expiry_format = '%a, %d-%b-%y %H:%M:%S GMT'


def cookie_props(env, age=None):
    #mapping = dict(domain=env.get('HTTP_HOST'), path='/')
    mapping = dict(path='/')
    if age is not None:
        mapping['max-age'] = age
    return mapping


def cookie_monster(app,
                   cookies_key='cookies',
                   raw_cookies_key='wsgi.cookies',
                   default_age=60*2):
    """ Creates an app for transparent request and response cookie handling.

    """
    def monster(environ, start_response):
        age_props = cookie_props(environ)
        expiry_props = cookie_props(environ, 0)

        res = WsgiCookie()
        req = environ[raw_cookies_key] = WsgiCookie(environ)
        cli = environ[cookies_key] = req.as_dict()
        state, rep_res = response_replacement()
        results = app(environ, rep_res)
        # deleted cookies
        for k in [k for k in req if k not in cli]:
            res[k] = ''
            res[k].update(expiry_props)

        ## modified cookie values
        for k, v in [(k, v) for k, v in cli.items()
                     if (k in req and cli[k] != req[k].value)]:
            if isinstance(v, dict):
                res[k] = v.pop('value', '')
                res[k].update(v)
            else:
                res[k] = v
                res[k].update(age_props)

        ## new cookie values
        for k, v in [(n, v) for n, v in cli.items() if n not in req]:
            if isinstance(v, dict):
                swapv = age_props.copy()
                swapv.update(v)
                v = swapv
            else:
                v = dict(value=v, **age_props)
            res[k] = v.pop('value', '')
            res[k].update(v)

        state[1].extend(res.wsgi_headers())
        start_response(*state)
        return results

    return monster


def cookie_crumbler(app, cookies_key='cookies', auth_cookie_key='a'):
    """ Creates an app to set HTTP_AUTH from a cookie value.

    >>> state, start_response = response_replacement()
    >>> app = cookie_crumbler(string_app('crumbled!'))
    >>> env = default_env({'cookies':{'a':'Aladdin:open sesame'}})
    >>> res = app(env, start_response)
    >>> env['HTTP_AUTHORIZATION']
    'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
    
    """
    def inner(environ, start_response):
        if environ.get(env_keys.http_auth) is None:
            auth = environ.get(cookies_key, {}).get(auth_cookie_key)
            if auth is not None:
                environ[env_keys.http_auth] = 'Basic %s' % auth.encode('base64')[0:-1]
        return app(environ, start_response)
    return inner


def monster_crumbler(app,
                     cookies_key='cookies',
                     auth_cookie_key='a',
                     enc_key=None):
    """ Combines a monster and a crumbler for transparent encryption,
        decryption, and conversion to HTTP basic auth.

    """
    c = cookie_crumbler(app, cookies_key, auth_cookie_key),
    m = cookie_monster(c, cookies_key) 
    return m




def _test():
    import doctest
    doctest.testmod(optionflags=doctest.ELLIPSIS)

if __name__ == "__main__":
    _test()

